/**
 * The `composio ts generate` command can be configured as follows:
 * - `--compact`: Emit a single module file
 * - `--transpiled`: Emit not just the TypeScript files, but also the transpiled JavaScript files
 * - `--output-dir`: Output directory for the generated TypeScript type stubs.
 *
 * Invariants:
 * - The `--output-dir` cannot refer to a path inside `node_modules`
 * - If `--output-dir` is not specified, the command will use `findComposioCoreGeneratedPath` to locate the `@composio/core` package
 *   and write to it inside `node_modules`
 * - The absence of `--output-dir` implies that, by default, `--transpiled` is set to true
 * - If `--output-dir` is specified, `--transpiled` is, by default, set to false
 * - If `--transpiled` evaluates to true, the sources generated by `generateTypeScriptSources` need to be transpiled to ESM JavaScript too,
 *   and stored along with the generated TypeScript files. CJS is not supported.
 */

import path from 'node:path';
import { Command, Options } from '@effect/cli';
import { Console, Effect, Option, pipe } from 'effect';
import { Match } from 'effect';
import { FileSystem } from '@effect/platform';
import { ComposioToolkitsRepository } from 'src/services/composio-clients';
import { NodeProcess } from 'src/services/node-process';
import { createToolkitIndex } from 'src/generation/create-toolkit-index';
import type { GetCmdParams } from 'src/type-utils';
import { generateTypeScriptSources } from 'src/generation/typescript/generate';
import { jsFindComposioCoreGenerated } from 'src/effects/find-composio-core-generated';
import { transpileTypeScriptSources } from 'src/generation/typescript/transpile';
import { BANNER } from 'src/generation/constants';

export const outputOpt = Options.optional(
  Options.directory('output-dir', {
    exists: 'either',
  })
).pipe(
  Options.withAlias('o'),
  Options.withDescription('Output directory for the generated TypeScript type stubs.')
);

export const compact = Options.boolean('compact').pipe(
  Options.withDefault(false),
  Options.withDescription('Emit a single TypeScript file')
);

export const transpiled = Options.boolean('transpiled').pipe(
  Options.withDefault(false),
  Options.withDescription('Whether to emit transpiled JavaScript alongside TypeScript files')
);

export const typeTools = Options.boolean('type-tools').pipe(
  Options.withDefault(false),
  Options.withDescription('Whether to emit type stubs for tools')
);

const _tsCmd$Generate = Command.make('generate', {
  outputOpt,
  compact,
  transpiled: transpiled,
  typeTools,
}).pipe(Command.withDescription('Updates the local type stubs with the latest app data.'));

/**
 * Validates that the output directory is not inside node_modules
 */
function validateOutputDir(outputDir: string): Effect.Effect<string, Error, FileSystem.FileSystem> {
  return Effect.gen(function* () {
    const normalizedPath = path.normalize(outputDir);

    if (normalizedPath.includes('node_modules')) {
      return yield* Effect.fail(
        new Error(
          'Output directory cannot be inside node_modules. Please specify a different directory.'
        )
      );
    }

    return outputDir;
  });
}

export const tsCmd$Generate = _tsCmd$Generate.pipe(
  Command.withHandler(params => {
    // Determine if we should compile based on the rules:
    // - If --output-dir is specified, default transpiled to false unless overridden
    // - If no --output-dir, default transpiled to true unless overridden
    const shouldCompile = params.transpiled || !Option.isSome(params.outputOpt);

    return generateTypescriptTypeStubs({
      ...params,
      compact: params.compact,
      transpiled: shouldCompile,
    });
  })
);

export function generateTypescriptTypeStubs({
  outputOpt,
  compact,
  typeTools,
  transpiled = false,
}: GetCmdParams<typeof _tsCmd$Generate> & { transpiled?: boolean }) {
  return Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const process = yield* NodeProcess;
    const cwd = process.cwd;
    const client = yield* ComposioToolkitsRepository;

    // Determine the actual output directory
    const outputDir = yield* outputOpt.pipe(
      Option.match({
        // If no output directory is specified, use the default, and make sure it exists
        onNone: () => jsFindComposioCoreGenerated(cwd),

        // If an output directory is specified, validate and create it
        onSome: outputDir => validateOutputDir(outputDir),
      })
    );

    yield* Effect.log(`Writing type stubs to ${outputDir}...`);
    yield* fs.makeDirectory(outputDir, { recursive: true });

    // Fetch data from Composio API
    yield* Console.log('Fetching latest data from Composio API. This may take a while...');

    const [triggerTypesAsEnums, toolsAsEnums] = yield* Effect.all([
      Effect.logDebug('Fetching trigger types...').pipe(
        Effect.flatMap(() => client.getTriggerTypesAsEnums())
      ),
      Effect.logDebug('Fetching tools...').pipe(Effect.flatMap(() => client.getToolsAsEnums())),
    ]);

    const [toolkits, triggerTypes] = yield* Effect.all(
      [
        Effect.logDebug('Fetching toolkits...').pipe(Effect.flatMap(() => client.getToolkits())),
        Effect.logDebug('Fetching trigger types payloads...').pipe(
          Effect.flatMap(() => client.getTriggerTypes(triggerTypesAsEnums.length))
        ),
      ],
      { concurrency: 'unbounded' }
    );

    // Only fetch tools if `--type-tools` was specified
    const typeableTools = yield* Match.value(typeTools).pipe(
      Match.when(true, typePredicate =>
        Effect.logDebug('Fetching tools...').pipe(
          Effect.flatMap(() => client.getTools(toolsAsEnums.length)),
          Effect.map(tools => ({
            withTypes: typePredicate,
            tools,
          }))
        )
      ),
      Match.when(false, typePredicate =>
        Effect.succeed({
          withTypes: typePredicate,
          tools: toolsAsEnums,
        })
      ),
      Match.exhaustive
    );

    yield* Console.log('Writing TypeScript type stubs to disk...');
    const index = createToolkitIndex({ toolkits, typeableTools, triggerTypes });

    // Generate TypeScript sources
    const sources = yield* generateTypeScriptSources({
      outputDir,
      emitSingleFile: Boolean(compact), // Ensure boolean type
      banner: BANNER,
      importExtension: 'js',
    })(index);

    // Write all generated files
    yield* pipe(
      Effect.all(
        sources.map(([filePath, content]) =>
          fs
            .writeFileString(filePath, content)
            .pipe(Effect.mapError(error => new Error(`Failed to write file ${filePath}: ${error}`)))
        ),
        { concurrency: 'unbounded' }
      ),
      Effect.mapError(error => new Error(`Failed to write generated files: ${error}`))
    );

    // Compile TypeScript to JavaScript if needed
    if (transpiled) {
      yield* pipe(
        transpileTypeScriptSources({ sources, outputDir }),
        Effect.catchAll(error =>
          Effect.logWarning(`Failed to compile TypeScript files: ${error.message}`)
        )
      );
    }

    yield* Option.isNone(outputOpt)
      ? Console.log(
          '✅ Type stubs generated successfully.\n' +
            'You can now import generated types via `import { Toolkits } from "@composio/core/generated"`'
        )
      : Console.log(
          `✅ Type stubs generated successfully.\n` +
            `Generated files are available at: ${outputDir}`
        );

    return outputDir;
  });
}
